/****************************************************************************
* Copyright (c) 2005-2006 Jeremy Dowdall
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* 	 IBM Corporation - SWT's CCombo was relied upon _heavily_ for example and reference
*    Jeremy Dowdall <jeremyd@aspencloud.com> - initial API and implementation
*****************************************************************************/

package org.aspencloud.widgets;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Widget;

/**
 * The DropButton is an abstract class which provides the basic functionality for a button with a 
 * DROP_DOWN, or "popup", shell component.  When the user selects the button the shell is set visible
 * and the SWT Components which have been placed on the "content" Composite will be shown.
 */
public abstract class DropButton extends Composite {

	Button 	button;
	Shell 	popup;
	Control content;

	private boolean dontPop = false;
	private boolean popped = false;
	private boolean holdOpen = false;
	private Control stretchControl;
	private Control positionControl;
	protected boolean leftAlign = false;
	protected boolean hasFocus;

	Listener listener, filter;
	
	/**
	 * The constructor is passed constants from the ACW class which must be converted
	 * to their corresponding SWT class constants for use in the super's constructor.
	 * No, this is not ideal, but does eliminate collisions during development.  Expect
	 * changes to this in the future.
	 * @param style constants from ACW.java
	 * @return constants from SWT.java
	 */
	private static int swtStyle(int style) {
		int s = ACW.gtk ? SWT.NONE : SWT.DOUBLE_BUFFERED;
		if((ACW.BORDER & style) != 0) s |= SWT.BORDER;
		return s;
	}

	
	public DropButton(Composite parent, int style) {
		super(parent, swtStyle(style));
		
		setLayout(new FillLayout());
		
		button = new Button(this, SWT.ARROW | SWT.DOWN);

		this.positionControl = button;
		
		listener = new Listener () {
			public void handleEvent (Event event) {
				for(Iterator i = getControls().iterator(); i.hasNext(); ) {
					Control control = (Control) i.next();
					if(control == event.widget) {
						controlEvents(event);
						return;
					}
				}
				for(Iterator i = getContents().iterator(); i.hasNext(); ) {
					Control control = (Control) i.next();
					if(control == event.widget) {
						contentsEvents(event);
						return;
					}
				}
				if(popup == event.widget) {
					popupEvents(event);
					return;
				}
				if(DropButton.this == event.widget) {
					dropButtonEvents(event);
					return;
				}
				if(DropButton.this.getShell () == event.widget) {
					handleFocus(SWT.FocusOut, event.widget);
				}
			}
		};
		
		filter = new Listener() {
			public void handleEvent(Event event) {
				Shell shell = ((Control)event.widget).getShell();
				if(shell == DropButton.this.getShell()) {
					handleFocus(SWT.FocusOut, event.widget);
				}
			}
		};


		int[] dropButtonEvents = { SWT.Dispose, SWT.Move, SWT.Resize };
		for(int i = 0; i < dropButtonEvents.length; i++) this.addListener (dropButtonEvents[i], listener);
		
		int [] controlEvents = {SWT.MouseDown, SWT.FocusIn};
		for (int i=0; i<controlEvents.length; i++) button.addListener (controlEvents [i], listener);
	}

	/**
	 * Event handler for the content Composite and its children.<br>
	 * TODO: re-evaluate
	 */
	protected void contentsEvents(Event event) {
			switch (event.type) {
			case SWT.Dispose:
				if(getShell () != popup.getParent ()) {
					popup = null;
					createPopup();
					setContent(content);
				}
				break;
			case SWT.FocusIn:
				handleFocus(event.type, event.widget);
				break;
			}
	}
	
	protected void controlEvents(Event event) {
		if(button == event.widget) {
			switch (event.type) {
			case SWT.FocusIn:
				handleFocus(event.type, event.widget);
				break;
			case SWT.MouseDown:
				if(ACW.gtk) {
					if(!dontPop) {
						popUp(!isPopped());
					}
					dontPop = false;
				} else {
					popUp(!isPopped());
				}
				break;
			}
		}
	}

	private void createPopup() {
		int pstyle = getParent().getShell().getStyle();
		GridLayout layout = new GridLayout();
		if(((pstyle & SWT.APPLICATION_MODAL) != 0) ||
				((pstyle & SWT.SYSTEM_MODAL) != 0)) {
			// TODO: find a fix other than setting popup's style to SWT.APPLICATION_MODAL
			popup = new Shell(getShell(), SWT.APPLICATION_MODAL);
			layout.marginWidth = 0;
			layout.marginHeight = 0;
		} else {
			popup = new Shell(getShell(), SWT.NO_TRIM | SWT.ON_TOP);
			layout.marginWidth = 1;
			layout.marginHeight = 1;
		}
		popup.setLayout(layout);
		
		int [] popupEvents = {SWT.Close, SWT.Deactivate, SWT.Paint };
		for (int i = 0; i < popupEvents.length; i++) popup.addListener (popupEvents[i], listener);
	}
	
	protected void dropButtonEvents(Event event) {
		switch (event.type) {
		case SWT.Dispose:
			if (popup != null && !popup.isDisposed ()) {
				content.removeListener(SWT.Dispose, listener);
				popup.dispose();
			}
			Shell shell = getShell();
			shell.removeListener (SWT.Deactivate, listener);
			Display display = getDisplay ();
			display.removeFilter(SWT.FocusIn, filter);
			shell = null;
			popup = null;
			content = null;
			button = null;
			positionControl = null;
			stretchControl = null;
			break;
		case SWT.Move:
			if(isPopped()) popUp(false);
			break;
		case SWT.Resize:
			if(isPopped()) popUp(false);
			layout(true);
			break;
		}
	}

	public Button getButton() {
		return button;
	}

	/**
	 * returns the Control that was set as this popup shell's content
	 * with setContent(Control)
	 */
	public Control getContent() {
		return content;
	}
	
	public List getContents() {
		if(content instanceof Composite) {
			return ACW.getControls((Composite) content);
		} else {
			return Collections.singletonList(content);
		}
	}
	
	private List getControls() {
		List l = new ArrayList();
		Object[] a = getChildren();
		for(int i = 0; i < a.length; i++) {
			if(a[i] instanceof Composite) {
				l.addAll(ACW.getControls((Composite) a[i]));
			} else {
				l.add(a[i]);
			}
		}
		return l;
	}
	
	public Shell getPopup() {
		if(popup == null) createPopup();
		return popup;
	}

	public Control getStretchControl() {
		return stretchControl;
	}
	
	/**
	 * The Focus event handler.  Subclasses may override but should always the super.
	 */
	protected void handleFocus(int type, Widget widget) {
		if(isDisposed()) return;
		switch (type) {
			case SWT.FocusIn: {
				if(hasFocus) return;
				hasFocus = true;
				Shell shell = getShell();
				shell.removeListener(SWT.Deactivate, listener);
				shell.addListener(SWT.Deactivate, listener);
				Display display = getDisplay();
				display.removeFilter(SWT.FocusIn, filter);
				display.addFilter(SWT.FocusIn, filter);
				Event event = new Event();
				notifyListeners(SWT.FocusIn, event);
				break;
			}
			case SWT.FocusOut: {
				if(!hasFocus) return;
				Control focusControl = getDisplay().getFocusControl();
				for(Iterator i = getControls().iterator(); i.hasNext(); ) {
					Control control = (Control) i.next();
					if(control == focusControl) return;
				}
				for(Iterator i = getContents().iterator(); i.hasNext(); ) {
					Control control = (Control) i.next();
					if(control == focusControl) return;
				}
				hasFocus = false;
				Shell shell = getShell();
				shell.removeListener(SWT.Deactivate, listener);
				Display display = getDisplay();
				display.removeFilter(SWT.FocusIn, filter);
				Event event = new Event ();
				notifyListeners (SWT.FocusOut, event);
				break;
			}
		}
	}

	/**
	 * returns the state of the holdOpen flag
	 */
	public boolean isHeldOpen() {
		return holdOpen;
	}
	
	/**
	 * returns the state of the popup shell's visibility
	 */
	public boolean isPopped() {
		return popped;
	}

	/**
	 * If pop is true, then opens the popup shell (sets to visible)<br>
	 * If pop is false, closes the popup shell (sets to not visible)<br>
	 * If <code>content == null</code> this method simply returns.<br>
	 * If <code>popup == null</code> then <code>popup</code> will be created.
	 * @param pop
	 */
	public void popUp(boolean pop) {
		if(content == null) return;
		if(popup == null) createPopup();
		
		if(getShell() != popup.getParent()) {
			content.removeListener(SWT.Dispose, listener);
			popup.dispose();
			popup = null;
			createPopup();
		}
		
		if(!pop) {
			if(!holdOpen) {
				popup.setVisible(false);
				popped = false;
			}
		} else {
			popup.pack();
			Point size = popup.computeSize(-1, -1);
			Point location = positionControl.toDisplay(positionControl.getLocation());
			location.y -= positionControl.getLocation().y - positionControl.getSize().y + 1;
			int dHeight = getDisplay().getClientArea().height;
//			if(location.y >= dHeight) return; // no room to popup, so don't bother...
			if((location.y + size.y) > dHeight) {
//				size.y = dHeight-location.y-30;
				location.y = positionControl.toDisplay(positionControl.getLocation()).y - size.y - 4;
			}
			if((stretchControl != null) && (size.x < stretchControl.getSize().x)) {
				size.x = stretchControl.getSize().x;
				popup.setSize(size);
			}
			
			if(leftAlign) {
				location.x -= positionControl.getLocation().x;
			} else {
				location.x -= positionControl.getLocation().x + size.x - positionControl.getSize().x;
				if(location.x < 0) location.x = 0;
			}
			if(ACW.win32) location.x -= 2;
			popup.setLocation(location);

			// chance for subclasses to do something before popup becomes visible
			aboutToPopUp(popup);
			
			popup.setVisible(true);
			content.setFocus();
			popped = true;
		}
	}

	/**
	 * called just before the popup is set visible and "pops up"
	 * <p>override if you want to do something with the popup prior to becoming visible</p>
	 */
	protected void aboutToPopUp(Shell popup) {}
	
	protected void popupEvents(Event event) {
		switch (event.type) {
		case SWT.Close:
			event.doit = false;
			popUp(false);
			break;
		case SWT.Deactivate:
			// check if deactivation happens due to activation of a component's menu
			boolean menuActive = false;
			for(Iterator i = getContents().iterator(); i.hasNext(); ) {
				Control control = (Control) i.next();
				if(control.getMenu() != null && control.getMenu().isVisible()) {
					menuActive = true;
					break;
				}
			}
			if(menuActive) break;
			
			// when the popup shell is deactivated by clicking the button,
			// we receive two Deactivate events on Win32, whereas on GTK
			// we first receive one Deactivation event from the shell and
			// then a Selection event from the button.
			// as a work-around, set a flag (dontPop) if running GTK
			if(ACW.gtk) {
				Point loc = button.toControl(getDisplay().getCursorLocation());
				Point size = button.getSize();
				if((loc.x >= 0) && (loc.y >= 0) && (loc.x < size.x) && (loc.y < size.y)) {
					dontPop = true;
				}
			}
			popUp(false);
			break;
		case SWT.Paint:
			event.gc.setForeground(event.display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
			event.gc.drawRectangle(event.x,event.y, event.width-1, event.height-1);
			break;
		}
	}
	
	/**
	 * set the content of the popup shell
	 * <p>Can be a single control, or a Composite consisting of many controls</p>
	 * @param popContents
	 */
	public void setContent(Control popContents) {
		if(popup == null) createPopup();
		
		if(this.content != null) {
			int[] contentsEvents = { SWT.Dispose, SWT.FocusIn, SWT.Selection };
			for(int i = 0; i < contentsEvents.length; i++) content.removeListener(contentsEvents[i], listener);
		}
		this.content = popContents;
		this.content.setParent(popup);
		this.content.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		int[] contentsEvents = { SWT.DefaultSelection, SWT.Dispose, SWT.FocusIn, SWT.Selection };
		for(int i = 0; i < contentsEvents.length; i++) {
			for(Iterator iter = getContents().iterator(); iter.hasNext(); ) {
				((Control) iter.next()).addListener(contentsEvents[i], listener);
			}
		}
	}

	public void setEnabled(boolean enabled) {
		button.setEnabled(enabled);
	}
	
	public boolean setFocus() {
		return button.setFocus();
	}

	/**
	 * if holdOpen is true, the popup shell will not close regardless of events and/or calls
	 * to popUp(false) until holdOpen is first set false
	 * <p>merely sets the holdOpen flag, does not change popup visibility state</p>
	 * @param holdOpen
	 */
	public void setHoldOpen(boolean holdOpen) {
		this.holdOpen = holdOpen;
	}
	
	/**
	 * sets the control to which the popup will align itself
	 * <p>the control does not necessarily need to be "this" or the button,
	 * but will default to "this" if positionControl == null</p>
	 * @param positionControl
	 */
	public void setPositionControl(Control positionControl) {
		if(positionControl == null) {
			this.positionControl = this;
		} else {
			this.positionControl = positionControl;
		}
	}

	public void setRedraw(boolean redraw) {
		button.setRedraw(redraw);
		if(popup != null) popup.setRedraw(redraw);
	}

	/**
	 * if stretchControl == null, the width of the popup will be
	 * set to its preferred width (via computeSize(SWT.DEFAULT, SWT.DEFAULT))
	 * <p>if stretchControl != null, the width of the popup will be
	 * stretched to equal the width of stretchControl (if, however,
	 * popup's preferred width is greater than stretchControl's width
	 * popup will not be shrunk down)</p>
	 * @param stretchControl
	 */
	public void setStretchControl(Control stretchControl) {
		this.stretchControl = stretchControl;
	}
}
